<?php
/**
 * YPasswordHelper.php
 *
 * @author Hua Yang <htmlcook@gmail.com>
 * @since 2.0
 */

/**
 * 参考drupal7 password.inc
 * 不同于CPasswordHelper 依赖crypt Blowfish 导致php5.3以下版本不能使用
 *
 * Class YPasswordHelper
 */
class YPasswordHelper
{
	const HASH_COUNT = 15;

	const MIN_HASH_COUNT = 7;

	const MAX_HASH_COUNT = 30;

	const HASH_LENGTH = 55;

	private static function _itoa64()
	{
		return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
	}

	private static function _base64Encode($input, $count)
	{
		$output = '';
		$i = 0;
		$itoa64 = self::_itoa64();
		do {
			$value = ord($input[$i++]);
			$output .= $itoa64[$value & 0x3f];
			if ($i < $count) {
				$value |= ord($input[$i]) << 8;
			}
			$output .= $itoa64[($value >> 6) & 0x3f];
			if ($i++ >= $count) {
				break;
			}
			if ($i < $count) {
				$value |= ord($input[$i]) << 16;
			}
			$output .= $itoa64[($value >> 12) & 0x3f];
			if ($i++ >= $count) {
				break;
			}
			$output .= $itoa64[($value >> 18) & 0x3f];
		} while ($i < $count);

		return $output;
	}

	private static function _generateSalt($count_log2)
	{
		$output = '$S$';
		// Ensure that $count_log2 is within set bounds.
		$count_log2 = self::_enforceLog2Boundaries($count_log2);
		// We encode the final log2 iteration count in base 64.
		$itoa64 = self::_itoa64();
		$output .= $itoa64[$count_log2];
		// 6 bytes is the standard salt for a portable phpass hash.
		/** @var CSecurityManager $securityManager */
		$securityManager = Yii::app()->getSecurityManager();
		$output .= self::_base64Encode($securityManager->generateRandomBytes(6), 6);
		return $output;
	}

	private static function _enforceLog2Boundaries($count_log2)
	{
		if ($count_log2 < self::MIN_HASH_COUNT) {
			return self::MIN_HASH_COUNT;
		} elseif ($count_log2 > self::MAX_HASH_COUNT) {
			return self::MAX_HASH_COUNT;
		}
		return (int)$count_log2;
	}

	private static function _crypt($algo, $password, $setting)
	{
		// The first 12 characters of an existing hash are its setting string.
		$setting = substr($setting, 0, 12);

		if ($setting[0] != '$' || $setting[2] != '$') {
			return false;
		}
		$count_log2 = self::_getCountLog2($setting);
		// Hashes may be imported from elsewhere, so we allow != self::HASH_COUNT
		if ($count_log2 < self::MIN_HASH_COUNT || $count_log2 > self::MAX_HASH_COUNT) {
			return false;
		}
		$salt = substr($setting, 4, 8);
		// Hashes must have an 8 character salt.
		if (strlen($salt) != 8) {
			return false;
		}
		// Convert the base 2 logarithm into an integer.
		$count = 1 << $count_log2;

		// We rely on the hash() function being available in PHP 5.2+.
		$hash = hash($algo, $salt . $password, true);
		do {
			$hash = hash($algo, $hash . $password, true);
		} while (--$count);

		$len = strlen($hash);
		$output = $setting . self::_base64Encode($hash, $len);
		// self::base64Encode() of a 16 byte MD5 will always be 22 characters.
		// self::base64Encode() of a 64 byte sha512 will always be 86 characters.
		$expected = 12 + ceil((8 * $len) / 6);
		return (strlen($output) == $expected) ? substr($output, 0, self::HASH_LENGTH) : false;
	}

	private static function _getCountLog2($setting)
	{
		$itoa64 = self::_itoa64();
		return strpos($itoa64, $setting[3]);
	}

	/**
	 * HASH密码
	 *
	 * @param $password
	 * @param int $count_log2
	 * @return bool|string
	 */
	public static function hashPassword($password, $count_log2 = 0)
	{
		if (empty($count_log2)) {
			// Use the standard iteration count.
			$count_log2 = Yii::app()->option->get('_password_count_log2', self::HASH_COUNT);
		}
		return self::_crypt('sha512', $password, self::_generateSalt($count_log2));
	}

	/**
	 * 验证密码
	 *
	 * @param $password
	 * @param $hashPassword
	 * @return bool
	 */
	public static function verifyPassword($password, $hashPassword)
	{
		$stored_hash = $hashPassword;
		$type = substr($stored_hash, 0, 3);

		if (strpos($type, '$') === false) {
			return md5($password) === $hashPassword;
		}

		switch ($type) {
			case '$S$':
				$hash = self::_crypt('sha512', $password, $stored_hash);
				break;
			case '$H$':
			case '$P$':
				// A phpass password generated using md5.  This is an
				// imported password or from an earlier Drupal version.
				$hash = self::_crypt('md5', $password, $stored_hash);
				break;
			default:
				return false;
		}
		return ($hash && $stored_hash == $hash);
	}

	/**
	 * 用户密码是否需要新hash
	 *
	 * @param $hashPassword
	 * @return bool
	 */
	public static function userNeedsNewHash($hashPassword)
	{
		// Check whether this was an updated password.
		if ((substr($hashPassword, 0, 3) != '$S$') || (strlen($hashPassword) != self::HASH_LENGTH)) {
			return true;
		}
		// Ensure that $count_log2 is within set bounds.
		$count_log2 = self::_enforceLog2Boundaries(Yii::app()->option->get('_password_count_log2', self::HASH_COUNT));
		// Check whether the iteration count used differs from the standard number.
		return (self::_getCountLog2($hashPassword) !== $count_log2);
	}
}